--[[------------------------------------------------------------------------------------------------ Универсальная боевая схема вертолёта Чугай Александр В отличие от сталкеров боевая схема не является отдельным действием, а вызывается из других схем. -- revamped by Alundaio Note: Heli Enemy Flag Is Set/Reset in axr_main, xr_combat_ignore, surge_manager, axr_radio_heli and heli_combat scripts Only flagged targets will be helicopter enemies during A-life heli simulation 8/18/2015: removed retreat combat type --------------------------------------------------------------------------------------------------]] local combat_type_flyby = 0 -- атака пролётами над целью local combat_type_round = 1 -- кружение вокруг позиции, атака кружась вокруг цели local combat_type_search = 2 -- поиск врага, кружение вокруг точки, где последний раз видел, атака кружась вокруг цели local combat_type_retreat = 3 -- улёт за пределы уровня local flyby_state_to_attack_dist = 0 local flyby_state_to_enemy = 1 -- пересечение луча и круга. -- p - точка начала луча, v - направление луча (орт), o - центр круга, r - радиус круга -- точка p должна быть внутри круга function cross_ray_circle( p, v, o, r ) local po = vector():set( o ):sub( p ) local vperp = vector():set( -v.z, 0, v.x ) local l = math.sqrt( ( r ^ 2 ) - ( vector():set( po ):dotproduct( vperp ) ^ 2 ) ) return vector():set( p ):add( vector():set( v ):mul( vector():set( po ):dotproduct( v ) + l ) ) end ---------------------------------------------------------------------------------------------------- class "heli_combat" function heli_combat:__init( object, heliObject ) self.st = db.storage[object:id()] self.object = object self.heliObject = heliObject self.initialized = false self.level_max_y = level.get_bounding_volume().max.y local section = object:section() self.flyby_attack_dist = ini_sys:r_float_ex(section,"flyby_attack_dist") or 0 self.search_attack_dist = ini_sys:r_float_ex(section,"search_attack_dist") or 0 self.default_safe_altitude = (ini_sys:r_float_ex(section,"safe_altitude") or 0) + self.level_max_y self.m_max_mgun_dist = ini_sys:r_float_ex(section,"max_mgun_attack_dist") or 0 self.default_velocity = ini_sys:r_float_ex(section,"velocity") or 30 self.search_velocity = ini_sys:r_float_ex(section,"search_velocity") or 40 self.round_velocity = ini_sys:r_float_ex(section,"round_velocity") or 60 self.vis_time_quant = ini_sys:r_float_ex(section,"vis_time_quant") or 0 self.vis_threshold = ini_sys:r_float_ex(section,"vis_threshold") or 0 self.vis_inc = (ini_sys:r_float_ex(section,"vis_inc") or 0) * self.vis_time_quant * 0.001 self.vis_dec = (ini_sys:r_float_ex(section,"vis_dec") or 0) * self.vis_time_quant * 0.001 self.vis = 0 self.vis_next_time = 0 self.forget_timeout = (ini_sys:r_float_ex(section,"forget_timeout") or 0) * 1000 self.flame_start_health = ini_sys:r_float_ex(section,"flame_start_health") or 0.1 self.attack_before_retreat = false self.section_changed = false self.combat_use_rocket = ini_sys:r_bool_ex(section,"combat_use_rocket",false) self.combat_use_mgun = ini_sys:r_bool_ex(section,"combat_use_mgun",true) self.actor_always_enemy= ini_sys:r_bool_ex(section,"actor_always_enemy",false) self.max_velocity = self.default_velocity self.safe_altitude = self.default_safe_altitude + self.level_max_y end function heli_combat:is_enemy(obj) --utils_data.debug_write(strformat("heli_combat:is_enemy",obj and obj:name())) if not (IsStalker(obj) or IsMonster(obj)) then return false end if not (obj:alive()) then return false end if (xr_combat_ignore.ignore_enemy_by_overrides(self.object,obj)) then return false end if (IsWounded(obj)) then return false end local se_obj = alife_object(self.object:id()) if not (se_obj and se_obj.community) then return false end if (obj:id() == AC_ID) then if (string.find(character_community(obj),se_obj.community)) then return false end if (load_var(db.actor,"heli_enemy_flag",false) == true) then return true end return self.actor_always_enemy or false else if (se_obj.community == character_community(obj)) then return false end end if (IsSurvivalMode()) then return true end if (not _G.WARFARE) then if not (db.storage[obj:id()] and db.storage[obj:id()].heli_enemy_flag) then return false end end local squad = obj:id() ~= 0 and get_object_squad(obj) local cid = squad and squad.id or id if (xr_combat_ignore.safe_zone_npcs[cid]) then return false end return true end function heli_combat:set_enemy(target) --utils_data.debug_write(strformat("heli_combat:set_enemy",target and target:name())) local tg = time_global() self.st.enemy_id = target:id() self.__keep_target_until = tg + 25000 self.enemy_last_seen_time = tg self.enemy_last_seen_pos = vector():set(target:position()) self.initialized = false self.heliObject:ClearEnemy() end function heli_combat:find_valid_target() --utils_data.debug_write(strformat("heli_combat:find_valid_target")) -- Evaluate current target local tg = time_global() local current_target = self.st.enemy_id and db.storage[self.st.enemy_id] and db.storage[self.st.enemy_id].object local current_target_visible if (current_target) then if not (self:is_enemy(current_target)) then self:forget_enemy() current_target = nil else current_target_visible = self:SeeEnemy() --self.heliObject:isVisible(current_target) if (current_target_visible) then -- Keep target for short duration to prevent over evaluating and to prevent constant target switching if (self.__keep_target_until and tg < self.__keep_target_until) then return true end db.storage[self.st.enemy_id].heli_enemy_flag = nil if (self.st.enemy_id == AC_ID) then save_var(db.actor,"heli_enemy_flag",false) end current_target = nil elseif (tg-self.enemy_last_seen_time > self.forget_timeout) then self:forget_enemy() current_target = nil end end end -- give priority to last hitted by target local hitted_by = db.storage[self.object:id()].hitted_by local who = hitted_by and db.storage[hitted_by] and db.storage[hitted_by].object if (who and self:is_enemy(who)) then self:set_enemy(who) db.storage[self.object:id()].hitted_by = nil return true end local size = #db.heli_enemies if (size == 0) then self:forget_enemy() return false end if (self.__eindex == nil or self.__eindex > size) then self.__eindex = 1 end local id = db.heli_enemies[self.__eindex] local target = id and db.storage[id] and db.storage[id].object self.__eindex = self.__eindex + 1 if (target == nil or not target:alive()) then return current_target ~= nil end if (current_target and id == self.st.enemy_id) then return true end if (self.heliObject:isVisible(target)) then local target_pos = target:position() if (id == AC_ID and load_var(db.actor,"heli_enemy_flag",false) and heli_alife.distance_2d_sqr(target:position(),self.object:position()) <= 10000) or (id ~= 0 and db.storage[id].heli_enemy_flag and heli_alife.distance_2d_sqr(target:position(),self.object:position()) <= 10000) then if (self.st.ini and self.st.active_section) then local smart = xr_gulag.get_npc_smart(self.object) if (smart) then local defend_smart = self.st.ini:r_bool_ex(self.st.active_section, "defend_job") if (defend_smart) then local radius = self.st.ini:r_float_ex(self.st.active_section,"defend_job_radius") or 50 if (radius) then local dist = heli_alife.distance_2d_sqr(target_pos,smart.position) if (dist < radius) then if (id == AC_ID) then save_var(db.actor,"heli_enemy_flag",true) else db.storage[target:id()].heli_enemy_flag = true end end end end end end end if (target and self:is_enemy(target)) then if (current_target and current_target_visible) then local pos = self.object:position() local dist = pos:distance_to_sqr(target_pos) local be_dist = pos:distance_to_sqr(current_target:position()) -- Only target the closest target if (dist < be_dist) then --printf("closer target %s",target and target:name()) self:set_enemy(target) return true end else self:set_enemy(target) return true end end end return current_target ~= nil end function heli_combat:read_custom_data( ini, section ) self.combat_use_rocket = ini:r_bool_ex(section,"combat_use_rocket") or ini_sys:r_bool_ex(section,"combat_use_rocket",false) self.combat_use_mgun = ini:r_bool_ex(section,"combat_use_mgun") or ini_sys:r_bool_ex(section,"combat_use_mgun",false) self.actor_always_enemy = ini:r_bool_ex(section,"actor_always_enemy") or ini_sys:r_bool_ex(section,"actor_always_enemy",false) self.combat_ignore = ini:r_string_to_condlist(section,"combat_ignore") local combat_enemy = ini:r_string_ex(section,"combat_enemy") self:set_enemy_from_custom_data( combat_enemy ) self.max_velocity = ini:r_float_ex(section,"combat_velocity") or self.default_velocity self.safe_altitude = (ini:r_float_ex(section,"combat_safe_altitude") or self.default_safe_altitude) + self.level_max_y self.section_changed = true end -- установка врага по custom data -- если враг установился этой функцией, то он не будет забываться при длительной потере видимости! -- если установился новый враг, то combat будет переинициализирован function heli_combat:set_enemy_from_custom_data(combat_enemy) if not (combat_enemy) then return end if (combat_enemy == "actor") then if db.actor then self.st.enemy_id = AC_ID else self:forget_enemy() end elseif (combat_enemy == "nil") then self:forget_enemy() else self.enemy = get_story_object(combat_enemy) self.st.enemy_id = self.enemy and self.enemy:id() end if (self.st.enemy_id) then self.enemy_from_custom_data = true self.initialized = false else self:forget_enemy() end end function heli_combat:set_combat_type(new_combat_type) --utils_data.debug_write(strformat("heli_combat:set_combat_type")) if (self.change_combat_type_allowed and new_combat_type ~= self.combat_type) then self.flyby_initialized = false self.round_initialized = false self.search_initialized = false self.combat_type = new_combat_type end end function heli_combat:initialize() self.section_changed = true self.combat_type = combat_type_flyby self.flyby_states_for_one_pass = 2 self.change_combat_type_allowed = true self.heliObject.m_min_mgun_dist = 15 self.heliObject.m_max_mgun_dist = self.m_max_mgun_dist self.object:set_fastcall( self.fastcall, self ) self.heliObject:TurnEngineSound(ui_options.get("alife/general/heli_engine_sound")) self.initialized = true self.flyby_initialized = false self.round_initialized = false self.search_initialized = false self.retreat_initialized = true end -- частое обновление. -- нужно для отслеживания видимости врага function heli_combat:fastcall() if self.initialized then if self.vis_next_time < time_global() then self.vis_next_time = time_global() + self.vis_time_quant local ene = self.st.enemy_id and db.storage[self.st.enemy_id] and db.storage[self.st.enemy_id].object if (ene and self.heliObject:isVisible(ene)) then self.vis = self.vis + self.vis_inc if self.vis > 100 then self.vis = 100 end else self.vis = self.vis - self.vis_dec if self.vis < 0 then self.vis = 0 end end end return false else return true end end function heli_combat:SeeEnemy() return self.enemy and self.vis >= self.vis_threshold end function heli_combat:save( packet ) --utils_data.debug_write(strformat("heli_combat:save")) -- packet:w_bool( self.retreat_already ) -- printf( "heli_combat:save level_changing=%s", tostring( level_changing() ) ) if level_changing() then packet:w_bool( false ) return end packet:w_bool( self.initialized ) if self.initialized then local t = time_global() packet:w_s16 (self.st.enemy_id) packet:w_u32 (t - (self.enemy_last_seen_time or 0)) packet:w_vec3(self.enemy_last_seen_pos or VEC_ZERO) packet:w_u8(self.combat_type) if self.combat_type == combat_type_search then packet:w_u32 ((self.change_pos_time or 0) - t) packet:w_bool(self.flight_direction or false) packet:w_vec3(self.center_pos or VEC_ZERO) elseif self.combat_type == combat_type_flyby then packet:w_s16( self.flyby_states_for_one_pass ) end end end function heli_combat:load( packet ) --utils_data.debug_write(strformat("heli_combat:load")) -- self.retreat_already = packet:r_bool() self.initialized = packet:r_bool() if self.initialized then self.initialized = false -- fix heli freezing after game load local t = time_global() self.st.enemy_id = packet:r_s16() self.enemy_last_seen_time = t - packet:r_u32() self.enemy_last_seen_pos = VEC_ZERO packet:r_vec3(self.enemy_last_seen_pos) self.combat_type = packet:r_u8() if self.combat_type == combat_type_search then self.change_pos_time = packet:r_u32() + t self.flight_direction = packet:r_bool() self.center_pos = VEC_ZERO packet:r_vec3(self.center_pos) elseif self.combat_type == combat_type_flyby then self.flyby_states_for_one_pass = packet:r_s16() end end end function heli_combat:waypoint_callback() if (self.st.enemy_id and not self:combat_ignore_check()) then self.was_callback = true return true end return false end -- Обновление параметров вертолёта, задаваемых в custom data. -- Нужно делать на каждом обновлении на случай, если во время боя логика переключилась на другую секцию. function heli_combat:update_custom_data_settings() if self.section_changed then self.heliObject.m_use_rocket_on_attack = self.combat_use_rocket self.heliObject.m_use_mgun_on_attack = self.combat_use_mgun if self.combat_type == combat_type_flyby then self.heliObject:SetMaxVelocity( self.max_velocity ) end self.section_changed = false end end function heli_combat:forget_enemy() --utils_data.debug_write(strformat("heli_combat:forget_enemy")) self.st.enemy_id = nil self.enemy = nil self.heliObject:LookAtPoint(VEC_Z, false) self.heliObject:ClearEnemy() self.initialized = false end function heli_combat:update_combat_type( see_enemy ) --utils_data.debug_write(strformat("heli_combat:update_combat_type")) if (self.combat_type == combat_type_flyby) then if not (see_enemy) then self:set_combat_type(combat_type_search) elseif (distance_2d(self.object:position(),self.enemy_last_seen_pos) < self.flyby_attack_dist) then self:set_combat_type(combat_type_round) end elseif (self.combat_type == combat_type_round) then if (see_enemy) then if (distance_2d(self.object:position(),self.enemy_last_seen_pos) > self.flyby_attack_dist) then self:set_combat_type(combat_type_flyby) end else self:set_combat_type(combat_type_search) end elseif (self.combat_type == combat_type_search) then if (see_enemy) then if (distance_2d(self.object:position(),self.enemy_last_seen_pos) > self.flyby_attack_dist) then self:set_combat_type(combat_type_flyby) elseif (distance_2d(self.object:position(),self.enemy_last_seen_pos) > self.search_attack_dist) then self:set_combat_type(combat_type_round) end end end end -- нужно ли игнорировать врага function heli_combat:combat_ignore_check() return self.combat_ignore ~= nil and xr_logic.pick_section_from_condlist( db.actor, self.object, self.combat_ignore ) == "true" end -- Обновление боевой схемы. Вызывается из обновлений схем логики вертолёта. -- возвращает true, если бой активен (то есть нету combat_ignore и есть враг) function heli_combat:update() if self:combat_ignore_check() then return false end --utils_data.debug_write(strformat("heli_combat:update start")) -- job is exclusive, use custom data logic for combat local smart = xr_gulag.get_npc_smart(self.object) local npc_info = smart and smart.npc_info[self.object:id()] if (npc_info and npc_info.job and npc_info.job.exclusive) then --utils_data.debug_write(strformat("heli_combat:update end 4")) return false end if not (self.enemy_from_custom_data) then if (not self:find_valid_target() or xr_conditions.surge_started() or not utils_obj.is_day()) then self:forget_enemy() --utils_data.debug_write(strformat("heli_combat:update end 3")) return false end end self.enemy = self.st.enemy_id and db.storage[self.st.enemy_id] and db.storage[self.st.enemy_id].object if not (self.enemy) then self:forget_enemy() --utils_data.debug_write(strformat("heli_combat:update end 2")) return false end if not self.initialized then self:initialize() end self:update_custom_data_settings() local see_enemy = self:SeeEnemy() --self.heliObject:isVisible(self.enemy) if (see_enemy) then local tg = time_global() --self.__keep_target_until = tg + 10000 self.enemy_last_seen_time = tg self.enemy_last_seen_pos = vector():set(self.enemy:position()) if (self.combat_use_mgun) then if (self.combat_type == combat_type_round) then if (self.round_begin_shoot_time == nil or tg > self.round_begin_shoot_time) then self.heliObject.m_use_mgun_on_attack = not self.heliObject.m_use_mgun_on_attack if (self.heliObject.m_use_mgun_on_attack) then self.round_begin_shoot_time = tg + math.random(4000,5000) else self.round_begin_shoot_time = tg + math.random(1000,2000) end end if (self.heliObject.m_use_mgun_on_attack and self.enemy) then self.heliObject:SetEnemy(self.enemy) else self.heliObject:ClearEnemy() end else self.heliObject:SetEnemy(self.enemy) end else self.heliObject:ClearEnemy() end else self.heliObject:ClearEnemy() end self:update_combat_type( see_enemy ) if self.combat_type == combat_type_search then self:search_update( see_enemy ) elseif self.combat_type == combat_type_round then self:round_update( see_enemy ) elseif self.combat_type == combat_type_flyby then self:flyby_update( see_enemy ) elseif self.combat_type == combat_type_retreat then self:retreat_update() end --utils_data.debug_write(strformat("heli_combat:update end 1")) return true end -- посчитать точку на заданном радиусе от последней видимой позиции врага в текущем направлении скорости вертолёта function heli_combat:calc_position_in_radius( r ) --utils_data.debug_write("heli_combat:calc_position_in_radius") local p = self.object:position() p.y = 0 local v = self.heliObject:GetCurrVelocityVec() v.y = 0 v:normalize() local o = self.enemy_last_seen_pos o.y = 0 local ret = cross_ray_circle( p, v, o, r ) ret.y = self.safe_altitude return ret end ---------------------------------------------------------------------------------------------- -- Фунциии кружащего боя ---------------------------------------------------------------------------------------------- function heli_combat:round_update(see_enemy) --utils_data.debug_write("heli_combat:round_update") local tg = time_global() if not self.round_initialized then self.change_combat_type_allowed = true self.heliObject:SetMaxVelocity(self.round_velocity) self.heliObject:SetSpeedInDestPoint(self.round_velocity) self.heliObject:UseFireTrail( false ) self.round_initialized = true self.__keep_state_until = tg + 10000 end if (self.__keep_state_until and tg < self.__keep_state_until) then self.change_combat_type_allowed = false else self.change_combat_type_allowed = true end if (self.change_speed_time and tg < self.change_speed_time) then return end self.change_speed_time = tg + 20000 local center_pos = vector():set(self.enemy_last_seen_pos) center_pos.y = self.safe_altitude self.heliObject:GoPatrolByRoundPath(center_pos,self.search_attack_dist, random_choice(true,false)) self.heliObject:LookAtPoint(center_pos,true) local v = math.random(5,self.round_velocity) self.heliObject:SetMaxVelocity(v) self.heliObject:SetSpeedInDestPoint(v) end ---------------------------------------------------------------------------------------------- -- Фунциии для поиска врага (скопировано с кружащего боя) ---------------------------------------------------------------------------------------------- function heli_combat:search_update( see_enemy ) --utils_data.debug_write("heli_combat:search_update") if not self.search_initialized then self.change_combat_type_allowed = true self.heliObject:UseFireTrail(false) self.search_initialized = true local center_pos = vector():set(self.enemy_last_seen_pos) center_pos.y = self.safe_altitude self.heliObject:GoPatrolByRoundPath(center_pos, self.search_attack_dist, random_choice(true,false)) self.heliObject:LookAtPoint(center_pos, true) end local tg = time_global() if (self.change_speed_time and tg < self.change_speed_time) then return end self.change_speed_time = tg + 5000 local v = math.random(2,self.search_velocity) self.heliObject:SetMaxVelocity(v) self.heliObject:SetSpeedInDestPoint(v) end ---------------------------------------------------------------------------------------------- -- Фунциии для боя с пролётами над целью ---------------------------------------------------------------------------------------------- function heli_combat:flyby_update( see_enemy ) --utils_data.debug_write("heli_combat:flyby_update") if not self.flyby_initialized then self.flyby_initialized = true self.heliObject:SetMaxVelocity(self.max_velocity) self.heliObject:SetSpeedInDestPoint(self.max_velocity) self.heliObject:LookAtPoint(VEC_ZERO,false) self.flyby_states_for_one_pass = 3 self.change_combat_type_allowed = false self.state = flyby_state_to_enemy self.fly_target_pos = vector():set(self.enemy_last_seen_pos) self.fly_target_pos.y = self.safe_altitude self.heliObject:SetDestPosition(self.fly_target_pos) else if (self.state == flyby_state_to_enemy and self.fly_target_pos and distance_2d( self.object:position(), self.fly_target_pos ) <= 30) then self.flyby_states_for_one_pass = self.flyby_states_for_one_pass - 1 self.state = flyby_state_to_attack_dist self.fly_target_pos = self:calc_position_in_radius(50) self.heliObject:SetDestPosition(self.fly_target_pos) if (self.flyby_states_for_one_pass <= 0) then self.change_combat_type_allowed = true else self.change_combat_type_allowed = false end elseif (self.state == flyby_state_to_attack_dist and self.fly_target_pos and distance_2d( self.object:position(), self.fly_target_pos ) <= 30) then self.state = flyby_state_to_enemy self.fly_target_pos = vector():set(self.enemy_last_seen_pos) self.fly_target_pos.y = self.safe_altitude self.heliObject:SetDestPosition(self.fly_target_pos) if not (see_enemy) then self.change_combat_type_allowed = true end end end self.change_combat_type_allowed = self.change_combat_type_allowed or distance_2d( self.object:position(), self.enemy_last_seen_pos ) < self.search_attack_dist end ---------------------------------------------------------------------------------------------- -- Фунциии для улетания за пределы уровня ---------------------------------------------------------------------------------------------- function heli_combat:retreat_initialize() self.retreat_initialized = true self.heliObject:SetMaxVelocity( self.max_velocity ) self.heliObject:SetSpeedInDestPoint( self.max_velocity ) self.heliObject:LookAtPoint( VEC_Z, false ) self.heliObject:SetDestPosition( self:calc_position_in_radius( 5000 ) ) end function heli_combat:retreat_update() --utils_data.debug_write("heli_combat:retreat_update") if not self.retreat_initialized then self:retreat_initialize() end end